package com.jaws.core.common.shard.interceptor;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.jaws.core.common.shard.algorithm.BaseShardAlgorithm;
import com.jaws.core.common.shard.util.DruidParse;
import com.jaws.core.common.shard.annotation.ShardSql;
import com.jaws.core.common.shard.domain.SqlParseResult;
import com.jaws.core.common.shard.domain.TableParse;
import com.jaws.core.common.shard.domain.TableShardInfo;
import com.jaws.core.configuration.ShardTableProperties;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;

/**
 * 增删改 分表逻辑处理
 *
 * @author liangqm
 * @date 2018-05-04 10:20:18
 * @since v1.0.0
 */
public abstract class BaseSqlInterceptor implements Interceptor, ApplicationContextAware {

	/**
	 * mybatis namespace id - < table , 分表信息（表名，分表列名）>
	 */
	private Map<String, SqlParseResult> namespaceCache = Maps.newConcurrentMap();

	private Map<String, Boolean> shardSqlCache = Maps.newConcurrentMap();

	/**
	 * spring 上下文
	 */
	private ApplicationContext applicationContext;

	/**
	 * 分表规则
	 */
	private Map<String, ShardTableProperties.TableRule> tableRuleMap;


	BaseSqlInterceptor(Map<String, ShardTableProperties.TableRule> tableRuleMap) {
		this.tableRuleMap = tableRuleMap;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	@Override
	public void setProperties(Properties properties) {
	}


	@Override
	public Object plugin(Object target) {
		if (target instanceof Executor) {
			return Plugin.wrap(target, this);
		}
		return target;
	}


	@Override
	public Object intercept(Invocation invocation) throws IllegalAccessException, InvocationTargetException, ClassNotFoundException {
		// invocation args
		Object[] args = invocation.getArgs();

		// statement
		MappedStatement mappedStatement = (MappedStatement) args[0];

		String namespace = mappedStatement.getId();

		// dao中传递的参数
		Object parameterObject = args[1];

		// 预编译sql
		BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);

		// 判断是否执行分表计算
		if (!decideShard(namespace)) {
			return invocation.proceed();
		}

		// 内存中取分表解析结果
		SqlParseResult sqlParseResult = obtainSqlParseResultFromCache(namespace, boundSql);

		// 分操作处理
		switch (sqlParseResult.getTableOperation()) {
			case Insert:
				handleNoWhereConditionsShardTable(invocation, mappedStatement, boundSql, parameterObject, sqlParseResult);
				break;
			case Update:
				handleWhereConditionsShardTable(invocation, mappedStatement, boundSql, sqlParseResult);
				break;
			case Delete:
				handleWhereConditionsShardTable(invocation, mappedStatement, boundSql, sqlParseResult);
				break;
			case Select:
				handleWhereConditionsShardTable(invocation, mappedStatement, boundSql, sqlParseResult);
				break;
			default:
				break;
		}

		// 执行
		return invocation.proceed();
	}

	private SqlParseResult obtainSqlParseResultFromCache(String namespace, BoundSql boundSql) throws IllegalAccessException {

		SqlParseResult sqlParseResult = namespaceCache.get(namespace);
		if (sqlParseResult == null) {
			sqlParseResult = generateSqlParseResult(boundSql);
			if (sqlParseResult == null) {
				throw new IllegalAccessException("解析SQL出错");
			}
			namespaceCache.put(namespace, sqlParseResult);
		}
		return sqlParseResult;
	}

	/**
	 * 判断是否执行分表
	 *
	 * @param namespace mybatis namespace id
	 * @return 是否执行分表
	 * @author liangqm
	 * @date 2018-05-10 13:48:17
	 * @since v1.0.0
	 */
	private Boolean decideShard(String namespace) throws ClassNotFoundException {
		Boolean result = shardSqlCache.get(namespace);
		if (result == null) {
			String className = namespace.substring(0, namespace.lastIndexOf("."));
			String methodName = namespace.substring(namespace.lastIndexOf(".") + 1, namespace.length());
			Method[] ms = Class.forName(className).getMethods();
			for (Method m : ms) {
				if (m.getName().equals(methodName)) {
					ShardSql annotation = m.getAnnotation(ShardSql.class);
					if (annotation == null) {
						shardSqlCache.put(namespace, false);
						result = false;
						break;
					}
					shardSqlCache.put(namespace, true);
					result = true;
					break;
				}
			}
		}
		return result;
	}

	private SqlParseResult generateSqlParseResult(BoundSql boundSql) {
		SqlParseResult sqlParseResult = new SqlParseResult();

		TableParse tableParse = DruidParse.parseSql(boundSql.getSql());
		sqlParseResult.setTableOperation(tableParse.getOperationType());

		Map<String, TableShardInfo> tableShardInfoMap = Maps.newHashMap();
		for (String table : tableParse.getTables()) {
			ShardTableProperties.TableRule tableRule = tableRuleMap.get(table);
			if (tableRule != null) {
				TableShardInfo tableShardInfo = new TableShardInfo();
				tableShardInfo.setTable(table);
				tableShardInfo.setBalanceColumn(tableRule.getBalanceColumn());
				tableShardInfo.setFullColumnName(tableShardInfo.getTable() + "." + tableShardInfo.getBalanceColumn());
				tableShardInfo.setFields(tableParse.getFields());
				tableShardInfo.setShardAlgorithm(tableRule.getShardAlgorithm());
				tableShardInfoMap.put(table, tableShardInfo);
			}
		}
		sqlParseResult.setTableShardInfoMap(tableShardInfoMap);

		return sqlParseResult;
	}


	/**
	 * 根据where条件进行分表计算(update,delete,select)
	 *
	 * @param invocation      ''
	 * @param mappedStatement ''
	 * @param boundSql        ''
	 * @param sqlParseResult  sql解析结果
	 * @throws   IllegalAccessException ''
	 * @throws   ClassNotFoundException ''
	 * @author liangqm
	 * @date 2018-05-04 9:04:18
	 * @since v1.0.0
	 */
	abstract void handleWhereConditionsShardTable(Invocation invocation, MappedStatement mappedStatement, BoundSql boundSql, SqlParseResult sqlParseResult) throws IllegalAccessException, ClassNotFoundException;

	/**
	 * 获取where条件对应的值
	 *
	 * @param mappedStatement statement
	 * @param boundSql        sql
	 * @param fullColumn      column
	 * @return column mapping value
	 * @author liangqm
	 * @date 2018-05-10 10:24:18
	 * @since v1.0.2
	 */
	Object obtainColumnValue(MappedStatement mappedStatement, BoundSql boundSql, String fullColumn) throws IllegalAccessException {

		// TODO: 2018/5/10  此处可优化: select * from test where user_id = #{userId} ,
		// 1.建立 namespace 下 user_id 与 userId 的对应关系
		// 2.查找 user_id 的值，通过 MetaObject metaObject = configuration.newMetaObject(parameterObject); Object obj = metaObject.getValue("userId");取值

		String actualSql = this.obtainActualSql(mappedStatement, boundSql);
		Map<String, List<Object>> conditionMap = DruidParse.parseSqlCondition(actualSql);
		Object value = findValue(fullColumn, conditionMap);
		if (value == null) {
			throw new IllegalAccessException("均衡字段没有值");
		}
		return value;
	}


	/**
	 * 处理没有where 条件的sql(insert)
	 *
	 * @param invocation      ''
	 * @param mappedStatement ''
	 * @param boundSql        ''
	 * @param parameterObject ''
	 * @param tableParse      sql解析结果
	 * @author liangqm
	 * @date 2018-05-04 9:04:18
	 * @since v1.0.2
	 */
	abstract void handleNoWhereConditionsShardTable(Invocation invocation, MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject, SqlParseResult tableParse) throws IllegalAccessException, ClassNotFoundException;


	/**
	 * 获取ybatis mapper sql #{} 对应的参数值
	 *
	 * @param mappedStatement ''
	 * @param boundSql        ''
	 * @param parameterObject ''
	 * @return mybatis mapper sql #{} 对应的参数值
	 * @author liangqm
	 * @date 2018-05-04 9:32:18
	 * @since v1.0.2
	 */
	List<Object> obtainInsertValues(MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject) {
		List<Object> valueList = Lists.newArrayList();

		// conf
		Configuration configuration = mappedStatement.getConfiguration();

		// TypeHandlerRegistry
		TypeHandlerRegistry typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();

		List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
		if (!parameterMappings.isEmpty()) {
			for (ParameterMapping parameterMapping : parameterMappings) {
				Object value;
				String propertyName = parameterMapping.getProperty();
				// issue #448 ask first for additional params
				if (boundSql.hasAdditionalParameter(propertyName)) {
					value = boundSql.getAdditionalParameter(propertyName);
				} else if (parameterObject == null) {
					value = null;
				} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
					value = parameterObject;
				} else {
					MetaObject metaObject = configuration.newMetaObject(parameterObject);
					value = metaObject.getValue(propertyName);
				}
				valueList.add(value);
			}
		}
		return valueList;
	}

	/**
	 * 创建SQL语句映射对象
	 *
	 * @param ms       MappedStatement
	 * @param sql      new sql
	 * @param boundSql BoundSql
	 * @return 新的MappedStatement
	 * @author maojian
	 * @date 2017-11-02 11:39:11
	 * @since v1.0.2
	 */
	MappedStatement makeMappedStatement(MappedStatement ms, String sql, BoundSql boundSql) {
		return createMappedStatement(ms, sql, boundSql);
	}


	/**
	 * 创建SQL语句对象
	 *
	 * @param configuration Configuration
	 * @param sql           sql
	 * @param boundSql      boundSql
	 * @return BoundSql
	 * @author maojian
	 * @date 2017-08-10 14:15:46
	 * @since v1.0.2
	 */
	private BoundSql createBoundSql(Configuration configuration, String sql, BoundSql boundSql) {
		return new BoundSql(configuration, sql, boundSql.getParameterMappings(), boundSql.getParameterObject());
	}

	/**
	 * 创建SQL语句映射对象
	 *
	 * @param ms       ''
	 * @param sql      ''
	 * @param boundSql ''
	 * @return MappedStatement
	 * @author maojian
	 * @date 2017-08-10 14:17:11
	 * @since v1.0.2
	 */
	@SuppressWarnings("unchecked")
	private MappedStatement createMappedStatement(MappedStatement ms, String sql, BoundSql boundSql) {
		BoundSql newBoundSql = createBoundSql(ms.getConfiguration(), sql, boundSql);
		try {
			// additionalParameters,metaParameters里面记录了临时变量和实际传参的对应关系
			Field additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
			additionalParametersField.setAccessible(true);
			Map<String, Object> additionalParameters = (Map<String, Object>) ReflectionUtils.getField(additionalParametersField, boundSql);
			ReflectionUtils.setField(additionalParametersField, newBoundSql, additionalParameters);

			Field metaParametersField = BoundSql.class.getDeclaredField("metaParameters");
			metaParametersField.setAccessible(true);
			MetaObject metaParameters = (MetaObject) ReflectionUtils.getField(metaParametersField, boundSql);
			ReflectionUtils.setField(metaParametersField, newBoundSql, metaParameters);
		} catch (NoSuchFieldException | SecurityException e) {
			e.printStackTrace();
		}

		return createMappedStatement(ms, new SqlSource() {

			private BoundSql boundSql;

			@Override
			public BoundSql getBoundSql(Object parameterObject) {
				return boundSql;
			}

			SqlSource setBoundSql(BoundSql boundSql) {
				this.boundSql = boundSql;
				return this;
			}
		}.setBoundSql(newBoundSql));
	}

	/**
	 * 创建SQL语句映射对象
	 *
	 * @param ms           原MappedStatement
	 * @param newSqlSource 新SqlSource
	 * @return MappedStatement
	 * @author maojian
	 * @date 2017-08-10 14:17:28
	 * @since v1.0.2
	 */
	private MappedStatement createMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
		String msId = ms.getId();
		MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), msId, newSqlSource, ms.getSqlCommandType());
		builder.resource(ms.getResource());
		builder.fetchSize(ms.getFetchSize());
		builder.statementType(ms.getStatementType());
		builder.keyGenerator(ms.getKeyGenerator());
		builder.timeout(ms.getTimeout());
		builder.parameterMap(ms.getParameterMap());
		builder.resultSetType(ms.getResultSetType());
		builder.cache(ms.getCache());
		builder.flushCacheRequired(ms.isFlushCacheRequired());
		builder.useCache(ms.isUseCache());
		// 结果映射
		builder.resultMaps(ms.getResultMaps());

		// 属性
		if (ms.getKeyProperties() != null && ms.getKeyProperties().length >= 1) {
			StringBuilder build = new StringBuilder();
			for (String key : ms.getKeyProperties()) {
				build.append(key);
				build.append(",");
			}
			if (build.length() > 0) {
				build.deleteCharAt(build.length() - 1);
			}
			builder.keyProperty(build.toString());
		}
		return builder.build();
	}


	/**
	 * 重设invocation参数
	 *
	 * @param invocation 调用
	 * @param ms         MappedStatement
	 * @author maojian
	 * @date 2017-11-02 11:37:31
	 * @since v1.0.2
	 */
	void resetInvocationArgs(Invocation invocation, MappedStatement ms) {
		Object[] args = invocation.getArgs();
		args[0] = ms;
	}


	/**
	 * 获取赋值后的sql
	 *
	 * @param mappedStatement mybatis Sql Statement
	 * @param boundSql        mybatis mapper sql
	 * @return 赋值后的sql
	 * @author liangqm
	 * @date 2018-05-04 8:54:18
	 * @since v1.0.2
	 */
	private String obtainActualSql(MappedStatement mappedStatement, BoundSql boundSql) {
		Configuration configuration = mappedStatement.getConfiguration();
		Object parameterObject = boundSql.getParameterObject();
		List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
		String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
		if (parameterMappings.size() > 0 && parameterObject != null) {
			TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
			if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
				sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));

			} else {
				MetaObject metaObject = configuration.newMetaObject(parameterObject);
				for (ParameterMapping parameterMapping : parameterMappings) {
					String propertyName = parameterMapping.getProperty();
					if (metaObject.hasGetter(propertyName)) {
						Object obj = metaObject.getValue(propertyName);

						String value = getParameterValue(obj);
						// values ('lampard-02', 'I1986618193006827524', '01', , '2018-05-07 09:58:47' );
						// ,, 会报错，使用 null 传入 ,null,
						if (StringUtils.isBlank(value)) {
							value = "null";
						}
						sql = sql.replaceFirst("\\?", value);
					} else if (boundSql.hasAdditionalParameter(propertyName)) {
						Object obj = boundSql.getAdditionalParameter(propertyName);
						sql = sql.replaceFirst("\\?", getParameterValue(obj));
					}
				}
			}
		}
		return sql;
	}

	private String getParameterValue(Object obj) {
		String value;
		if (obj instanceof String) {
			value = "'" + obj.toString() + "'";
		} else if (obj instanceof Date) {
			DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
			value = "'" + formatter.format(obj) + "'";
		} else {
			if (obj != null) {
				value = obj.toString();
			} else {
				value = "";
			}

		}
		return value;
	}


	private Object findValue(String columnFullName, Map<String, List<Object>> conditionsMap) {
		List<Object> columnValues = conditionsMap.get(columnFullName);
		// full name（table.name） 匹配
		if (!columnValues.isEmpty()) {
			return columnValues.get(0);
		}
		String columnName = columnFullName.substring(columnFullName.lastIndexOf(".") + 1, columnFullName.length());
		// full name（table.name）没有找到对应的值，使用name 进行匹配
		for (Map.Entry<String, List<Object>> entry : conditionsMap.entrySet()) {
			String fullName = entry.getKey();
			if (fullName.indexOf(columnName) > 0) {
				if (!entry.getValue().isEmpty()) {
					return entry.getValue().get(0);
				}
			}
		}
		return null;
	}

	/**
	 * 计算分表，返回分表后表名
	 *
	 * @param tableShardInfo 分表信息
	 * @param table          原表名（逻辑表名）
	 * @param value          分表字段值
	 * @return 分表后表名 table${segment}
	 * @author liangqm
	 * @date 2018-05-07 18:55:18
	 * @since v1.0.0
	 */
	String deduceNewTable(TableShardInfo tableShardInfo, String table, Object value) throws ClassNotFoundException {

		String algorithmClassName = tableShardInfo.getShardAlgorithm();
		if (StringUtils.isBlank(algorithmClassName)) {
			// 默认算法，分8张表
			algorithmClassName = "com.jaws.core.common.shard.algorithm.BaseShardAlgorithm";
		}

		BaseShardAlgorithm shardAlgorithm = (BaseShardAlgorithm) applicationContext.getBean(Class.forName(algorithmClassName));
		return shardAlgorithm.getTableSegment(table, value.toString());
	}
}
